﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using System.IO.Compression;
using System.Collections;

namespace Netcached
{
    class Serializer
    {
        public int CompressionThreshold { get; set; }
        public CompressionAlgorithm CompressionAlgorithm { get; set; }

        public SerializationResult Serialize(object o)
        {
            SerializationResult result = null;
            byte[] bytes = null;
            SerializedType type = SerializedType.UNDEF;
            if (o is byte[])
            {
                bytes = (byte[])o;
                type = SerializedType.BYTEARRAY;
            }
            else if (o is int)
            {
                bytes = BitConverter.GetBytes((int)o);
                type = SerializedType.INT;
            }
            else if (o is uint)
            {
                bytes = BitConverter.GetBytes((uint)o);
                type = SerializedType.UINT;
            }
            else if (o is short)
            {
                bytes = BitConverter.GetBytes((short)o);
                type = SerializedType.SHORT;
            }
            else if (o is ushort)
            {
                bytes = BitConverter.GetBytes((ushort)o);
                type = SerializedType.USHORT;
            }
            else if (o is long)
            {
                bytes = BitConverter.GetBytes((long)o);
                type = SerializedType.LONG;
            }
            else if (o is ulong)
            {
                bytes = BitConverter.GetBytes((ulong)o);
                type = SerializedType.ULONG;
            }
            else if (o is float)
            {
                bytes = BitConverter.GetBytes((float)o);
                type = SerializedType.FLOAT;
            }
            else if (o is double)
            {
                bytes = BitConverter.GetBytes((double)o);
                type = SerializedType.DOUBLE;
            }
            else if (o is char)
            {
                bytes = BitConverter.GetBytes((char)o);
                type = SerializedType.CHAR;
            }
            else if (o is bool)
            {
                bytes = BitConverter.GetBytes((bool)o);
                type = SerializedType.BOOL;
            }
            else if (o is string)
            {
                bytes = Encoding.UTF8.GetBytes((string)o);
                type = SerializedType.STRING;
            }
            else if (o is decimal)
            {
                bytes = Encoding.UTF8.GetBytes(((decimal)o).ToString());
                type = SerializedType.DECIMAL;
            }
            else if (o is DateTime)
            {
                bytes = BitConverter.GetBytes(((DateTime)o).Ticks);
                type = SerializedType.DATETIME;
            }
            else
            {
                var formatter = new BinaryFormatter();
                var memoryStream = new MemoryStream();
                formatter.Serialize(memoryStream, o);
                bytes = memoryStream.ToArray();
                type = SerializedType.OBJECT;
                memoryStream.Close();
            }

            bool useCompression = CompressionThreshold > 0 && bytes.Length > CompressionThreshold;
            if (useCompression)
            {
                bytes = Compress(bytes);
            }

            if (type != SerializedType.UNDEF && bytes != null && bytes.Length > 0)
            {
                int flags = BuildSerializationFlags(type, useCompression);
                result = new SerializationResult() { Bytes = bytes, SerializationFlags = flags };
            }

            return result;
        }

        public object Deserialize(byte[] bytes, int serializationFlags)
        {
            object returnObject = null;

            SerializedType type;
            bool useCompression;
            CompressionAlgorithm compressionAlgorithm;

            ExtractSerializationFlags(serializationFlags, out type, out useCompression, out compressionAlgorithm);

            if (useCompression)
            {
                bytes = Decompress(bytes, compressionAlgorithm);
            }

            if (bytes != null && bytes.Length > 0 && type != SerializedType.UNDEF)
            {
                if (type == SerializedType.BYTEARRAY)
                {
                    returnObject = bytes;
                }
                else if (type == SerializedType.INT)
                {
                    returnObject = BitConverter.ToInt32(bytes, 0);
                }
                else if (type == SerializedType.UINT)
                {
                    returnObject = BitConverter.ToUInt32(bytes, 0);
                }
                else if (type == SerializedType.SHORT)
                {
                    returnObject = BitConverter.ToInt16(bytes, 0);
                }
                else if (type == SerializedType.USHORT)
                {
                    returnObject = BitConverter.ToUInt16(bytes, 0);
                }
                else if (type == SerializedType.LONG)
                {
                    returnObject = BitConverter.ToInt64(bytes, 0);
                }
                else if (type == SerializedType.ULONG)
                {
                    returnObject = BitConverter.ToUInt64(bytes, 0);
                }
                else if (type == SerializedType.FLOAT)
                {
                    returnObject = BitConverter.ToSingle(bytes, 0);
                }
                else if (type == SerializedType.DOUBLE)
                {
                    returnObject = BitConverter.ToDouble(bytes, 0);
                }
                else if (type == SerializedType.CHAR)
                {
                    returnObject = BitConverter.ToChar(bytes, 0);
                }
                else if (type == SerializedType.BOOL)
                {
                    returnObject = BitConverter.ToBoolean(bytes, 0);
                }
                else if (type == SerializedType.STRING)
                {
                    returnObject = Encoding.UTF8.GetString(bytes);
                }
                else if (type == SerializedType.DECIMAL)
                {
                    returnObject = decimal.Parse(Encoding.UTF8.GetString(bytes));
                }
                else if (type == SerializedType.DATETIME)
                {
                    returnObject = new DateTime(BitConverter.ToInt64(bytes, 0));
                }
                else if (type == SerializedType.OBJECT)
                {
                    var formatter = new BinaryFormatter();
                    var memoryStream = new MemoryStream(bytes);
                    returnObject = formatter.Deserialize(memoryStream);
                    memoryStream.Close();
                }
            }
            return returnObject;
        }

        private byte[] Compress(byte[] data)
        {
            byte[] bytes = null;

            using (var tempStream = new MemoryStream())
            {
                Stream compressionStream = null;
                if (CompressionAlgorithm == Netcached.CompressionAlgorithm.Deflate)
                {
                    compressionStream = new DeflateStream(tempStream, CompressionMode.Compress);
                }
                else if (CompressionAlgorithm == Netcached.CompressionAlgorithm.GZip)
                {
                    compressionStream = new GZipStream(tempStream, CompressionMode.Compress);
                }

                using (compressionStream)
                {
                    compressionStream.Write(data, 0, data.Length);
                }

                bytes = tempStream.ToArray();
            }

            return bytes;
        }

        private byte[] Decompress(byte[] data, CompressionAlgorithm compressionAlgorithm)
        {
            byte[] bytes = null;

            Stream decompressionStream = null;
            if (CompressionAlgorithm == Netcached.CompressionAlgorithm.Deflate)
            {
                decompressionStream = new DeflateStream(new MemoryStream(data), CompressionMode.Decompress);
            }
            else if (CompressionAlgorithm == Netcached.CompressionAlgorithm.GZip)
            {
                decompressionStream = new GZipStream(new MemoryStream(data), CompressionMode.Decompress);
            }

            using (decompressionStream)
            {
                using (var tempStream = new MemoryStream())
                {
                    decompressionStream.CopyTo(tempStream);
                    bytes = tempStream.ToArray();
                }
            }

            return bytes;
        }

        private int BuildSerializationFlags(SerializedType serializedType, bool useCompression)
        {
            var serializedTypeBytes = System.BitConverter.GetBytes((int)serializedType);
            BitArray flags = new BitArray(serializedTypeBytes);

            // reserve decimal values 0 - 63 (bits 0 to 5) for type information
            // use bit 6 to store information whether compresion is enabled/disabled
            flags[6] = useCompression;

            // use bit 7 to store information about compression algorithm. Reserve 8 for future expansion.
            // Store 0 for Deflate, 1 for GZip
            flags[7] = CompressionAlgorithm == CompressionAlgorithm.GZip;

            int[] array = new int[1];
            flags.CopyTo(array, 0);

            return array[0];
        }

        private void ExtractSerializationFlags(int flags, out SerializedType type, out bool useCompression, out CompressionAlgorithm compressionAlgorithm)
        {
            BitArray bitArray = new BitArray(new int[] { flags });

            BitArray typeBitArray = new BitArray(bitArray);
            typeBitArray[6] = false;
            typeBitArray[7] = false;

            byte[] typeByteArray = new byte[4];
            typeBitArray.CopyTo(typeByteArray, 0);

            type = (SerializedType)BitConverter.ToInt32(typeByteArray, 0);

            useCompression = bitArray[6];
            compressionAlgorithm = bitArray[7] ? CompressionAlgorithm.GZip : Netcached.CompressionAlgorithm.Deflate;
        }
    }

    class SerializationResult
    {
        public byte[] Bytes { get; set; }
        public int SerializationFlags { get; set; }
    }

    enum SerializedType
    {
        UNDEF = 0,
        BYTEARRAY = 1,
        SBYTE = 2,
        INT = 3,
        UINT = 4,
        SHORT = 5,
        USHORT = 6,
        LONG = 7,
        ULONG = 8,
        FLOAT = 9,
        DOUBLE = 10,
        CHAR = 11,
        BOOL = 12,
        STRING = 13,
        DECIMAL = 14,
        DATETIME = 15,
        OBJECT = 16
    }

    public enum CompressionAlgorithm
    {
        Deflate,
        GZip
    }
}
